__all__ = ['parse_command_line', 'log_config', 'init', 'schedule_tasks', 'clean']


import argparse
import base64
import datetime
import logging
import os
import shutil
import ssl
import socket
import sys
import types

import util


__author__  = 'ZL <837806295@qq.com>'
__product__ = 'comparethem'
__version__ = '1.0.5'
__date__    = '2023/04/15'


log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler())  # 如果宿主未配置日志处理器，则丢弃日志

args = types.SimpleNamespace()

password = None

local_head = 'path://local'
description_head = 'description://'
description_extension = '.description'
difference_extension = '.difference'


def is_regular_file(path):
    """是否常规文件
    """

    return (os.path.isfile(path) and not os.path.islink(path))

def is_regular_dir(path):
    """是否常规目录
    """

    return (os.path.isdir(path) and not os.path.islink(path))

def get_dir_regular_content(path, ignore_hide):
    """非递归地获取目录中常规子目录与常规文件
    """

    if (not isinstance(path, str)
            or not path
            or not os.path.isdir(path)):
        log.critical('get dir regular content ... failed ... exit | illegal '
                     'input')
        sys.exit(1)

    content = set()

    try:
        for entry in os.listdir(path):
            entry_path = os.path.join(path, entry)

            if util.is_hide(entry) and ignore_hide:
                log.info('get dir regular content ... ongoing | ignore: %s',
                         entry_path)
                continue

            if is_regular_file(entry_path) or is_regular_dir(entry_path):
                content.add(entry)
                log.debug('get dir regular content ... ongoing | hit: %s',
                          entry_path)
            else:
                log.debug('get dir regular content ... ongoing | not hit: %s',
                          entry_path)
    except Exception:
        log.exception('get dir regular content ... exception ... exit')
        sys.exit(1)

    log.debug('get dir regular content ... successed | content: %s', content)
    return content

def copy_regular_file(src, dest):
    """复制常规文件
    """

    if (not isinstance(src, str)
            or not src
            or not isinstance(dest, str)
            or not dest):
        log.critical('copy regular file ... failed ... exit | illegal input')
        sys.exit(1)

    if not os.path.isfile(src):
        log.error('copy regular file ... failed | src | not file: %s', src)
        return False

    if os.path.islink(src):
        log.error('copy regular file ... failed | src | symbolic link: %s',
                  src)
        return False

    if os.path.exists(dest) and not is_regular_file(dest):
        log.error('copy regular file ... failed | dest | exist and not regular '
                  'file : %s',
                  dest)
        return False

    dest_dir = os.path.split(dest)[0]

    try:
        os.makedirs(dest_dir, exist_ok=True)
    except Exception:
        log.exception('copy regular file ... exception ... exit')
        sys.exit(1)

    log.debug('copy regular file ... ongoing | src: %s, dest: %s', src, dest)

    try:
        shutil.copyfile(src, dest)
    except Exception:
        log.exception('copy regular file ... exception ... exit')
        sys.exit(1)

    log.debug('copy regular file ... successed')
    return True

def copy_regular_dir_once(src, dest, ignore_hide, subdir=None, failed=None):
    """非递归的复制目录
    """

    if (not isinstance(src, str)
            or not src
            or not isinstance(dest, str)
            or not dest):
        log.critical('copy regular dir once ... failed ... exit | illegal '
                     'input')
        sys.exit(1)

    if not os.path.isdir(src):
        if isinstance(failed, list):
            failed.append((src, dest))

        log.error('copy regular dir once ... failed | src | not dir: %s', src)
        return False

    if os.path.islink(src):
        if isinstance(failed, list):
            failed.append((src, dest))

        log.error('copy regular dir once ... failed | src | symbolic link: %s',
                  src)
        return False

    if os.path.exists(dest) and not is_regular_dir(dest):
        if isinstance(failed, list):
            failed.append((src, dest))

        log.error('copy regular dir once ... failed | dest | exist and not '
                  'regular dir : %s',
                  dest)
        return False

    try:
        os.makedirs(dest, exist_ok=True)
    except Exception:
        log.exception('copy regular dir once ... exception ... exit')
        sys.exit(1)

    ok = True

    entries = get_dir_regular_content(src, ignore_hide)
    for entry in entries:
        src_path = os.path.join(src, entry)
        dest_path = os.path.join(dest, entry)

        log.debug('copy regular dir once ... ongoing | src path: %s, '
                  'dest_path: %s',
                  src_path,
                  dest_path)

        if os.path.isfile(src_path):
            if copy_regular_file(src_path, dest_path):
                log.debug('copy regular dir once ... ongoing | file entry | '
                          'copy %s to %s... successed',
                          src_path,
                          dest_path)
            else:
                if ok:
                    ok = False

                if isinstance(failed, list):
                    failed.append((src_path, dest_path))

                log.error('copy regular dir once ... ongoing | file entry | '
                          'copy %s to %s ... failed',
                          src_path,
                          dest_path)
        else:
            if isinstance(subdir, list):
                subdir.append((src_path, dest_path))

            log.debug('copy regular dir once ... ongoing | dir entry | '
                      'src path: %s, dest_path: %s',
                      src_path,
                      dest_path)

    if ok:
        log.debug('copy regular dir once ... successed | subdir: %s, '
                  'failed: %s',
                  subdir,
                  failed)
    else:
        log.error('copy regular dir once ... failed | subdir: %s, failed: %s',
                  subdir,
                  failed)

    return ok

def copy_regular_dir(src, dest, ignore_hide, failed=None):
    """递归复制常规目录的常规内容
    """

    if (not isinstance(src, str)
            or not src
            or not isinstance(dest, str)
            or not dest):
        log.critical('copy regular dir ... failed ... exit | illegal input')
        sys.exit(1)

    if not os.path.isdir(src):
        if isinstance(failed, list):
            failed.append((src, dest))

        log.error('copy regular dir ... failed | src | not dir: %s', src)
        return False

    if os.path.islink(src):
        if isinstance(failed, list):
            failed.append((src, dest))

        log.error('copy regular dir ... failed | src | symbolic link: %s', src)
        return False

    ok = True

    dirs = [(src, dest)]
    while dirs:
        log.debug('copy regular dir ... ongoing | dirs: %s', dirs)

        pair = dirs.pop(0)
        subdir = []
        if copy_regular_dir_once(pair[0],
                                 pair[1],
                                 ignore_hide,
                                 subdir,
                                 failed):
            log.debug('copy regular dir ... ongoing | copy once | '
                      'copy %s to %s ... successed',
                      pair[0],
                      pair[1])
        else:
            if ok:
                ok = False

            log.error('copy regular dir ... ongoing | copy once | '
                      'copy %s to %s ... failed',
                      pair[0],
                      pair[1])

        dirs += subdir

    if ok:
        log.debug('copy regular dir ... successed')
    else:
        log.error('copy regular dir ... failed')

    return ok

def remove_regular_file(path):
    """移除常规文件
    """

    if (not isinstance(path, str)
            or not path
            or not os.path.isfile(path)):
        log.critical('remove regular file ... failed ... exit | illegal input')
        sys.exit(1)

    if os.path.islink(path):
        log.error('remove regular file ... failed | symbolic link: %s', path)
        return False

    try:
        os.remove(path)
    except Exception:
        log.exception('remove regular file ... exception ... exit')
        sys.exit(1)

    log.debug('remove regular file ... successed')
    return True

def remove_regular_dir(path):
    """递归移除常规目录
    """

    if (not isinstance(path, str)
            or not path
            or not os.path.isdir(path)):
        log.critical('remove regular dir ... failed ... exit | illegal input')
        sys.exit(1)

    if os.path.islink(path):
        log.error('remove regular dir ... failed | symbolic link: %s', path)
        return False

    try:
        shutil.rmtree(path)
    except Exception:
        log.exception('remove regular dir ... exception ... exit')
        sys.exit(1)

    log.debug('remove regular dir ... successed')
    return True

def is_local_path(path):
    """是否本地路径，这里路径是列表式的
    """

    if (isinstance(path, list)
            and path
            and isinstance(path[0], str)
            and path[0]):
        return True
    else:
        return False

def is_list_file(path):
    """是否文件，这里路径是列表式的
    """

    if (isinstance(path, list)
            and path
            and isinstance(path[0], str)
            and os.path.isfile(normalize(path))):
        return True

    if (isinstance(path, list)
            and path
            and isinstance(path[0], dict)
            and is_description_file(path)):
        return True

    return False

def is_list_dir(path):
    """是否目录，这里路径是列表式的
    """

    if (isinstance(path, list)
            and path
            and isinstance(path[0], str)
            and os.path.isdir(normalize(path))):
        return True

    if (isinstance(path, list)
            and path
            and isinstance(path[0], dict)
            and is_description_dir(path)):
        return True

    return False

def normalize(path):
    """将列表式路径规范化的转换到字符串路径
    """

    if (not isinstance(path, list)
            or not path
            or not (isinstance(path[0], str) or isinstance(path[0], dict))):
        log.critical('normalize ... failed ... exit | illegal input')
        sys.exit(1)

    if isinstance(path[0], str):
        path2 = path[0]

        for name in path[1:]:
            if isinstance(name, str) and name:
                path2 = os.path.join(path2, name)
            else:
                log.critical('normalize ... failed ... exit | invalid input')
                sys.exit(1)
    elif isinstance(path[0], dict):
        name = get_description_name(path[0])
        path2 = '%s%s' % (description_head, name)

        for name in path[1:]:
            if isinstance(name, str) and name:
                path2 += '%s%s' % ('/', name)
            else:
                log.critical('normalize ... failed ... exit | invalid input')
                sys.exit(1)
    else:
        log.critical('normalize ... failed ... exit | invalid input')
        sys.exit(1)

    log.debug('normalize ... successed | path2: %s, path: %s', path2, path)
    return path2

def join(path, *args):
    """连接列表式路径与子项生成新列表式路径
    """

    if (not isinstance(path, list)
            or not path
            or not (isinstance(path[0], str) or isinstance(path[0], dict))):
        log.critical('join ... failed ... exit | illegal input')
        sys.exit(1)

    path2 = list(path)
    for x in args:
        if isinstance(x, str) and x:
            path2.append(x)
        else:
            log.critical('join ... failed ... exit | invalid input')
            sys.exit(1)

    log.debug('join ... done | path2: %s, path: %s', path2, path)
    return path2

def virtualize(path):
    """将列表式的路径虚化

    将列表式的路径虚化，会把第一项替换为字符串，生成新列表式路径
    """

    if (not isinstance(path, list)
            or not path
            or not (isinstance(path[0], str) or isinstance(path[0], dict))):
        log.critical('virtualize ... failed ... exit | illegal input')
        sys.exit(1)

    if isinstance(path[0], str):
        path2 = join(path)
        path2[0] = local_head
    elif isinstance(path[0], dict):
        path2 = join(path)
        name = get_description_name(path[0])
        path2[0] = '%s%s' % (description_head, name)
    else:
        log.critical('virtualize ... failed ... exit | invalid input')
        sys.exit(1)

    log.debug('virtualize ... successed | path2: %s, path: %s', path2, path)
    return path2

def realize(root, path):
    """将虚化的列表式路径实化

    将虚化的列表式路径实化，会把第一项替换为本地目录，生成新列表式路径
    """

    if (not isinstance(root, str)
            or not root
            or not isinstance(path, list)
            or not path
            or not (isinstance(path[0], str) or isinstance(path[0], dict))):
        log.critical('realize ... failed ... exit | illegal input')
        sys.exit(1)

    path2 = join(path)
    path2[0] = root

    log.debug('realize ... successed | path2: %s, path: %s', path2, path)
    return path2

def count_changes_between_dirs(path, path2, common, ignore_file_content,
                               bytes_per_read):
    """统计变化的项的数量，只保留还需要进一步比较的目录项，这里的路径要是列表式的
    """

    if (not isinstance(path, list)
            or not path
            or not os.path.isdir(normalize(path))
            or not isinstance(path2, list)
            or not path2
            or not os.path.isdir(normalize(path2))
            or not isinstance(common, set)
            or not isinstance(bytes_per_read, int)
            or bytes_per_read <= 0):
        log.critical('count changes between dirs ... failed ... exit | illegal '
                     'input')
        sys.exit(1)

    log.debug('count changes between dirs ... ongoing | common: %s', common)

    changes = 0
    need_discard = set()
    for entry in common:
        entry_path = os.path.join(normalize(path), entry)
        entry_path2 = os.path.join(normalize(path2), entry)

        if os.path.isdir(entry_path) and os.path.isdir(entry_path2):
            log.debug('count changes between dirs ... ongoing | not hit: %s',
                      entry)
        elif os.path.isfile(entry_path) and os.path.isfile(entry_path2):
            need_discard.add(entry)

            if ignore_file_content:
                file_size = os.path.getsize(entry_path)
                if file_size == os.path.getsize(entry_path2):
                    log.debug('count changes between dirs ... ongoing | '
                              'not hit: %s',
                              entry)
                else:
                    changes += 1
                    log.debug('count changes between dirs ... ongoing | '
                              'hit: %s',
                              entry)
            else:
                if util.compare_files(entry_path, entry_path2, bytes_per_read):
                    log.debug('count changes between dirs ... ongoing | '
                              'not hit: %s',
                              entry)
                else:
                    changes += 1
                    log.debug('count changes between dirs ... ongoing | '
                              'hit: %s',
                              entry)
        elif os.path.isfile(entry_path) and os.path.isdir(entry_path2):
            need_discard.add(entry)

            changes += 1
            log.debug('count changes between dirs ... ongoing | hit: %s', entry)
        elif os.path.isdir(entry_path) and os.path.isfile(entry_path2):
            need_discard.add(entry)

            changes += 1
            log.debug('count changes between dirs ... ongoing | hit: %s', entry)
        else:
            log.critical('count changes between dirs ... failed ... exit | '
                         'should not happen')
            sys.exit(1)

    common.difference_update(need_discard)
    log.debug('count changes between dirs  ... ongoing | common: %s, '
              'need discard: %s',
              common,
              need_discard)

    log.debug('count changes between dirs ... successed | changes: %d', changes)
    return changes

def get_changes_between_dirs(path, path2, common, ignore_file_content,
                             bytes_per_read):
    """统计变化的项，只保留还需要进一步比较的目录项，这里的路径要是列表式的
    """

    if (not isinstance(path, list)
            or not path
            or not os.path.isdir(normalize(path))
            or not isinstance(path2, list)
            or not path2
            or not os.path.isdir(normalize(path2))
            or not isinstance(common, set)
            or not isinstance(bytes_per_read, int)
            or bytes_per_read <= 0):
        log.critical('get changes between dirs ... failed ... exit | illegal '
                     'input')
        sys.exit(1)

    log.debug('get changes between dirs ... ongoing | common: %s', common)

    changes = []
    need_discard = set()
    for entry in common:
        entry_path = os.path.join(normalize(path), entry)
        entry_path2 = os.path.join(normalize(path2), entry)

        if os.path.isdir(entry_path) and os.path.isdir(entry_path2):
            log.debug('get changes between dirs ... ongoing | not hit: %s',
                      entry)
        elif os.path.isfile(entry_path) and os.path.isfile(entry_path2):
            need_discard.add(entry)

            if ignore_file_content:
                file_size = os.path.getsize(entry_path)
                if file_size == os.path.getsize(entry_path2):
                    log.debug('get changes between dirs ... ongoing | '
                              'not hit: %s',
                              entry)
                else:
                    changes.append([join(path, entry), join(path2, entry)])
                    log.debug('get changes between dirs ... ongoing | hit: %s',
                              entry)
            else:
                if util.compare_files(entry_path, entry_path2, bytes_per_read):
                    log.debug('get changes between dirs ... ongoing | '
                              'not hit: %s',
                              entry)
                else:
                    changes.append([join(path, entry), join(path2, entry)])
                    log.debug('get changes between dirs ... ongoing | hit: %s',
                              entry)
        elif os.path.isfile(entry_path) and os.path.isdir(entry_path2):
            need_discard.add(entry)

            changes.append([join(path, entry), join(path2, entry)])
            log.debug('get changes between dirs ... ongoing | hit: %s', entry)
        elif os.path.isdir(entry_path) and os.path.isfile(entry_path2):
            need_discard.add(entry)

            changes.append([join(path, entry), join(path2, entry)])
            log.debug('get changes between dirs ... ongoing | hit: %s', entry)
        else:
            log.critical('get changes between dirs ... failed ... exit | '
                         'should not happen')
            sys.exit(1)

    common.difference_update(need_discard)
    log.debug('get changes between dirs ... ongoing | common: %s, '
              'need discard: %s',
              common,
              need_discard)

    log.debug('get changes between dirs ... successed | changes: %s', changes)
    return changes

def extend_entries(path, path2, entries):
    """将目录的共有内容集中每项扩展为完整路径对，这里路径要用列表式的
    """

    if (not isinstance(path, list)
            or not path
            or not (isinstance(path[0], str) or isinstance(path[0], dict))
            or not isinstance(path2, list)
            or not path2
            or not (isinstance(path2[0], str) or isinstance(path2[0], dict))
            or not isinstance(entries, set)):
        log.critical('extend entries ... failed ... exit | illegal input')
        sys.exit(1)

    entries2 = []
    for entry in entries:
        entries2.append([join(path, entry), join(path2, entry)])

    log.debug('join ... done | entries2: %s, entries: %s', entries2, entries)
    return entries2

def compare_once(path, path2, ignore_hide, ignore_file_content, bytes_per_read,
                 add, delete, change):
    """非递归比较两个目录，统计差异项数量，这里路径要用列表式的
    """

    if (not isinstance(path, list)
            or not path
            or not os.path.isdir(normalize(path))
            or not isinstance(path2, list)
            or not path2
            or not os.path.isdir(normalize(path2))
            or not isinstance(bytes_per_read, int)
            or bytes_per_read <= 0
            or not isinstance(add, list)
            or not add
            or not isinstance(add[0], int)
            or not isinstance(delete, list)
            or not delete
            or not isinstance(delete[0], int)
            or not isinstance(change, list)
            or not change
            or not isinstance(change[0], int)):
        log.critical('compare once ... failed ... exit | illegal input')
        sys.exit(1)

    content = get_dir_regular_content(normalize(path), ignore_hide)
    content2 = get_dir_regular_content(normalize(path2), ignore_hide)

    log.debug('compare once ... ongoing | content: %s, content2: %s',
              content,
              content2)

    common = content & content2
    add2 = content2 - content
    delete2 = content - content2

    log.debug('compare once ... ongoing | common: %s, add2: %s, delete2: %s',
              common,
              add2,
              delete2)

    add[0] += len(add2)
    delete[0] += len(delete2)
    change[0] += count_changes_between_dirs(path, path2, common,
                                            ignore_file_content, bytes_per_read)

    log.debug('compare once ... successed | common: %s', common)
    return common

def compare_once2(path, path2, ignore_hide, ignore_file_content, bytes_per_read,
                  add, delete, change):
    """非递归比较两个目录，统计差异项，这里路径要用列表式的
    """

    if (not isinstance(path, list)
            or not path
            or not os.path.isdir(normalize(path))
            or not isinstance(path2, list)
            or not path2
            or not os.path.isdir(normalize(path2))
            or not isinstance(bytes_per_read, int)
            or bytes_per_read <= 0
            or not isinstance(add, list)
            or not isinstance(delete, list)
            or not isinstance(change, list)):
        log.critical('compare once ... failed ... exit | illegal input')
        sys.exit(1)

    content = get_dir_regular_content(normalize(path), ignore_hide)
    content2 = get_dir_regular_content(normalize(path2), ignore_hide)

    log.debug('compare once ... ongoing | content: %s, content2: %s',
              content,
              content2)

    common = content & content2
    add2 = content2 - content
    delete2 = content - content2

    log.debug('compare once ... ongoing | common: %s, add2: %s, delete2: %s',
              common,
              add2,
              delete2)

    add.extend(extend_entries(path, path2, add2))
    delete.extend(extend_entries(path, path2, delete2))
    change.extend(
        get_changes_between_dirs(path, path2, common, ignore_file_content,
                                 bytes_per_read))

    log.debug('compare once ... successed | common: %s', common)
    return common

def compare_dirs(path, path2, ignore_hide, ignore_file_content, bytes_per_read,
                 add, delete, change):
    """递归比较两个目录，统计差异项数量
    """

    if (not isinstance(path, str)
            or not path
            or not os.path.isdir(path)
            or not isinstance(path2, str)
            or not path2
            or not os.path.isdir(path2)
            or not isinstance(bytes_per_read, int)
            or bytes_per_read <= 0
            or not isinstance(add, list)
            or not add
            or not isinstance(add[0], int)
            or not isinstance(delete, list)
            or not delete
            or not isinstance(delete[0], int)
            or not isinstance(change, list)
            or not change
            or not isinstance(change[0], int)):
        log.critical('compare dirs ... failed ... exit | illegal input')
        sys.exit(1)

    log.debug('compare dirs ... ongoing')

    dirs = [[[path], [path2]]]
    while dirs:
        log.debug('compare dirs ... ongoing | dirs: %s', dirs)

        pair = dirs.pop(0)
        common = compare_once(pair[0], pair[1], ignore_hide,
                              ignore_file_content, bytes_per_read,
                              add, delete, change)
        dirs += extend_entries(pair[0], pair[1], common)

    log.debug('compare dirs ... done')

def compare_dirs2(path, path2, ignore_hide, ignore_file_content, bytes_per_read,
                  add, delete, change):
    """递归比较两个目录，统计差异项
    """

    if (not isinstance(path, str)
            or not path
            or not os.path.isdir(path)
            or not isinstance(path2, str)
            or not path2
            or not os.path.isdir(path2)
            or not isinstance(bytes_per_read, int)
            or bytes_per_read <= 0
            or not isinstance(add, list)
            or not isinstance(delete, list)
            or not isinstance(change, list)):
        log.critical('compare dirs ... failed ... exit | illegal input')
        sys.exit(1)

    log.debug('compare dirs ... ongoing')

    dirs = [[[path], [path2]]]
    while dirs:
        log.debug('compare dirs ... ongoing | dirs: %s', dirs)

        pair = dirs.pop(0)
        common = compare_once2(pair[0], pair[1], ignore_hide,
                               ignore_file_content, bytes_per_read,
                               add, delete, change)
        dirs += extend_entries(pair[0], pair[1], common)

    log.debug('compare dirs ... done')

def classify_change(change):
    """把变化分为类型变化与内容变化，其中路径都是列表式的

    文件变成目录，或者目录变成文件，这些是类型变化，如果是文件的内容发生变化，则是内容变化，这
    里路径都是列表式的，会返回规范化的路径
    """

    if not isinstance(change, list):
        log.critical('classify change ... failed ... exit | illegal input')
        sys.exit(1)

    type_change = []
    content_change = []
    for x in change:
        path = x[0]
        path2 = x[1]

        if is_list_dir(path) and is_list_file(path2):
            type_change.append(normalize(path))
            continue

        if is_list_file(path) and is_list_dir(path2):
            type_change.append(normalize(path))
            continue

        if is_list_file(path) and is_list_file(path2):
            content_change.append(normalize(path))
            continue

        log.critical('classify change ... failed ... exit | should not happen')
        sys.exit(1)

    log.debug('classify change ... done | type change: %s, content change: %s',
              type_change,
              content_change)
    return (type_change, content_change)

def classify_paths(paths, base=True):
    """把路径集分为目录和文件，其中路径都是列表式的，会返回规范化的路径
    """

    if not isinstance(paths, list):
        log.critical('classify paths ... failed ... exit | illegal input')
        sys.exit(1)

    dirs = []
    files = []
    for x in paths:
        if base:
            path = x[0]
        else:
            path = x[1]

        if is_list_dir(path):
            dirs.append(normalize(path))
            continue

        if is_list_file(path):
            files.append(normalize(path))
            continue

        log.critical('classify paths ... failed ... exit | should not happen')
        sys.exit(1)

    log.debug('classify paths ... done | dirs: %s, files: %s', dirs, files)
    return (dirs, files)

def add_entries(add, ignore_hide, failed=None):
    """添加目录项，这里路径要是实化的
    """

    if not isinstance(add, list):
        log.critical('add entries ... failed ... exit | illegal input')
        sys.exit(1)

    log.info('add entries ... ongoing')

    for x in add:
        path = normalize(x[0])
        path2 = normalize(x[1])

        if os.path.isfile(path2):
            prompt_status2('Add file', 'ongoing', path)
            ok = copy_regular_file(path2, path)
            prompt_assert(ok, 'Add file', path, False)

            if not ok and isinstance(failed, list):
                failed.append((path2, path))
        elif os.path.isdir(path2):
            prompt_status2('Add dir', 'ongoing', path)
            ok = copy_regular_dir(path2, path, ignore_hide, failed)
            prompt_assert(ok, 'Add dir', path, False)
        else:
            log.critical('add entries ... failed ... exit | should not happen')
            sys.exit(1)

    log.info('add entries ... done')

def delete_entries(delete, failed=None):
    """删除目录项，这里路径要是实化的
    """

    if not isinstance(delete, list):
        log.critical('delete entries ... failed ... exit | illegal input')
        sys.exit(1)

    log.info('delete entries ... ongoing')

    for x in delete:
        path = normalize(x[0])

        if os.path.isfile(path):
            ok = remove_regular_file(path)
            prompt_assert(ok, 'Delete file', path, False)

            if not ok and isinstance(failed, list):
                failed.append(path)
        elif os.path.isdir(path):
            ok = remove_regular_dir(path)
            prompt_assert(ok, 'Delete dir', path, False)

            if not ok and isinstance(failed, list):
                failed.append(path)
        else:
            log.critical('delete entries ... failed ... exit | should not '
                         'happen')
            sys.exit(1)

    log.info('delete entries ... done')

def change_entries(change, ignore_hide, delete_failed, copy_failed):
    """变更目录项，这里路径要是实化的
    """

    if not isinstance(change, list):
        log.critical('change entries ... failed ... exit | illegal input')
        sys.exit(1)

    log.info('change entries ... ongoing')

    for x in change:
        path = normalize(x[0])
        path2 = normalize(x[1])

        if os.path.isfile(path) and os.path.isfile(path2):
            prompt_status2('Change file', 'ongoing', path)
            ok = copy_regular_file(path2, path)
            prompt_assert(ok, 'Change file', path, False)

            if not ok and isinstance(copy_failed, list):
                copy_failed.append((path2, path))
        elif os.path.isdir(path) and os.path.isfile(path2):
            ok = remove_regular_dir(path)
            prompt_assert(ok, 'Delete dir', path, False)

            if not ok:
                if isinstance(delete_failed, list):
                    delete_failed.append(path)

                continue

            prompt_status2('Add file', 'ongoing', path)
            ok = copy_regular_file(path2, path)
            prompt_assert(ok, 'Add file', path, False)

            if not ok and isinstance(copy_failed, list):
                copy_failed.append((path2, path))
        elif os.path.isfile(path) and os.path.isdir(path2):
            ok = remove_regular_file(path)
            prompt_assert(ok, 'Delete file', path, False)

            if not ok:
                if isinstance(delete_failed, list):
                    delete_failed.append(path)

                continue

            prompt_status2('Add dir', 'ongoing', path)
            ok = copy_regular_dir(path2, path, ignore_hide, copy_failed)
            prompt_assert(ok, 'Add dir', path, False)
        else:
            log.critical('change entries ... failed ... exit | should not '
                         'happen')
            sys.exit(1)

    log.info('change entries ... done')

def generate_file_description(path, name, disable_digest, bytes_per_read):
    """生成文件的描述信息
    """

    if (not isinstance(path, str)
            or not path
            or not os.path.isfile(path)
            or not isinstance(name, str)
            or not name
            or not isinstance(bytes_per_read, int)
            or bytes_per_read <= 0):
        log.critical('generate file description ... failed ... exit | illegal '
                     'input')
        sys.exit(1)

    log.debug('generate file description ... ongoing | path: %s', path)

    sha512 = None
    if not disable_digest:
        sha512 = util.get_file_sha512(path, bytes_per_read)

    description = {
        'type': 'file',
        'name': base64.urlsafe_b64encode(name.encode('utf-8')).decode('utf-8'),
        'size': os.path.getsize(path),
        'sha512': sha512
    }

    log.debug('generate file description ... successed | disable digest: %s',
              disable_digest)
    return description

def generate_dir_description(path, name, ignore_hide, disable_digest,
                             bytes_per_read):
    """生成目录的描述信息
    """

    if (not isinstance(path, str)
            or not path
            or not os.path.isdir(path)
            or not isinstance(name, str)
            or not name
            or not isinstance(bytes_per_read, int)
            or bytes_per_read <= 0):
        log.critical('generate dir description ... failed ... exit | illegal '
                     'input')
        sys.exit(1)

    description = {
        'type': 'dir',
        'name': base64.urlsafe_b64encode(name.encode('utf-8')).decode('utf-8')
    }

    content = []

    try:
        for entry in os.listdir(path):
            entry_path = os.path.join(path, entry)

            if util.is_hide(entry) and ignore_hide:
                log.info('generate dir description ... ongoing | ignore: %s',
                         entry_path)
                continue

            if is_regular_file(entry_path):
                file_description = generate_file_description(entry_path,
                                                             entry,
                                                             disable_digest,
                                                             bytes_per_read)
                content.append(file_description)
                log.debug('generate dir description ... ongoing | hit: %s',
                          entry_path)
            elif is_regular_dir(entry_path):
                dir_description = generate_dir_description(entry_path,
                                                           entry,
                                                           ignore_hide,
                                                           disable_digest,
                                                           bytes_per_read)
                content.append(dir_description)
                log.debug('generate dir description ... ongoing | hit: %s',
                          entry_path)
            else:
                log.debug('generate dir description ... ongoing | not hit: %s',
                          entry_path)
    except Exception:
        log.exception('generate dir description ... exception ... exit')
        sys.exit(1)

    description['content'] = content

    log.debug('generate dir description ... successed')
    return description

def check_description_file(entry):
    """检查是否符合目录描述信息中文件项要求
    """

    if not isinstance(entry, dict) or not entry:
        log.critical('check description file ... error ... exit | illegal '
                     'input')
        sys.exit(1)

    if ({'type', 'name', 'size', 'sha512'} <= set(entry.keys())
            and entry['type'] == 'file'
            and isinstance(entry['name'], str)
            and isinstance(entry['size'], int)
            and (entry['sha512'] is None or isinstance(entry['sha512'], str))):
        log.debug('check description file ... ok')
        return True
    else:
        log.debug('check description file ... error')
        return False

def check_description_dir(entry):
    """检查是否符合目录描述信息中的目录项要求
    """

    if not isinstance(entry, dict) or not entry:
        log.critical('check description dir ... error ... exit | illegal input')
        sys.exit(1)

    if ({'type', 'name', 'content'} <= set(entry.keys())
            and entry['type'] == 'dir'
            and isinstance(entry['name'], str)
            and isinstance(entry['content'], list)):
        log.debug('check description dir ... ok')
        return True
    else:
        log.debug('check description dir ... error')
        return False

def restore_description(description):
    """恢复description内容

    恢复description内容，主要是解base64编码
    """

    entries = [description]
    while entries:
        log.debug('restore description ... ongoing | entries: %s', entries)

        entry = entries.pop(0)

        if check_description_file(entry):
            name = base64.urlsafe_b64decode(entry['name']).decode('utf-8')
            entry['name'] = name
            log.debug('restore description ... ongoing | name: %s', name)
        elif check_description_dir(entry):
            name = base64.urlsafe_b64decode(entry['name']).decode('utf-8')
            entry['name'] = name
            log.debug('restore description ... ongoing | name: %s', name)

            entries += entry['content']
        else:
            log.critical('restore description ... failed ... exit | invalid '
                         'input')
            sys.exit(1)

    log.debug('restore description ... done | description: %s', description)
    return description

def get_description_name(description):
    """获取目录描述信息的指代名称
    """

    if (not isinstance(description, dict)
            or not description
            or 'name' not in description.keys()):
        log.critical('get description name ... failed ... exit | illegal input')
        sys.exit(1)

    name = description['name']
    if not isinstance(name, str) or not name:
        log.critical('get description name ... failed ... exit | invalid input')
        sys.exit(1)

    return name

def is_description_path(path):
    """是否目录描述信息中路径，这里路径是列表式的
    """

    if (isinstance(path, list)
            and path
            and isinstance(path[0], dict)
            and path[0]):
        return True
    else:
        return False

def get_description_entry(path):
    """获取路径对应描述项，这里路径是列表式的
    """

    if not is_description_path(path):
        log.critical('get description entry ... failed ... exit | illegal '
                     'input')
        sys.exit(1)

    description = path[0]
    if description['type'] != 'dir':
        log.critical('get description entry ... failed ... exit | invalid '
                     'input')
        sys.exit(1)

    for name in path[1:]:
        if description['type'] != 'dir':
            log.error('get description entry ... failed | parent '
                      'description: %s', description)
            return {}

        if not isinstance(name, str) and not name:
            log.error('get description entry ... failed | name: %s', name)
            return {}

        ok = False
        entries = description['content']
        for entry in entries:
            if entry['name'] == name:
                description = entry
                ok = True
                break

        if not ok:
            log.error('get description entry ... failed | nonexistent')
            return {}

    log.debug('get description entry ... successed | description: %s',
              description)
    return description

def is_description_file(path):
    """是否目录描述信息中文件，这里路径是列表式的
    """

    description = get_description_entry(path)
    if description and description['type'] == 'file':
        return True
    else:
        return False

def is_description_dir(path):
    """是否目录描述信息中目录，这里路径是列表式的
    """

    description = get_description_entry(path)
    if description and description['type'] == 'dir':
        return True
    else:
        return False

def get_description_dir_content(path, ignore_hide):
    """非递归获取目录描述信息中目录的内容，这里路径是列表式的
    """

    description = get_description_entry(path)
    if not description or description['type'] != 'dir':
        log.critical('get description dir content ... failed ... exit | '
                     'illegal input')
        sys.exit(1)

    content = set()

    entries = description['content']
    for entry in entries:
        type_ = entry['type']
        name = entry['name']

        entry_path = normalize(join(path, name))

        if util.is_hide(name) and ignore_hide:
            log.info('get description dir content ... ongoing | ignore: %s',
                     entry_path)
            continue

        if type_ in ['file', 'dir']:
            content.add(name)
            log.debug('get description dir content ... ongoing | hit: %s',
                      entry_path)
        else:
            log.critical('get description dir content ... failed ... exit | '
                         'invalid input')
            sys.exit(1)

    log.debug('get description dir content ... successed | content: %s',
              content)
    return content

def get_description_file_size(path):
    """获取目录描述信息中文件的大小，这里路径是列表式的
    """

    description = get_description_entry(path)
    if not description or description['type'] != 'file':
        log.critical('get description file size ... failed ... exit | illegal '
                     'input')
        sys.exit(1)

    size = description['size']

    log.debug('get description file size ... successed | size: %s', size)
    return size

def get_description_file_sha512(path):
    """获取目录描述信息中文件的SHA512，这里路径是列表式的
    """

    description = get_description_entry(path)
    if not description or description['type'] != 'file':
        log.critical('get description file sha512 ... failed ... exit | '
                     'illegal input')
        sys.exit(1)

    sha512 = description['sha512']

    log.debug('get description file sha512 ... successed | sha512: %s', sha512)
    return sha512

def compare_file_with_description(path, path2, bytes_per_read):
    """非递归的比较本地文件与目录描述信息中的文件

    非递归的比较本地文件与目录描述信息中的文件，本地路径是字符串路径，目录描述信息中路径是列表
    式的
    """

    if (not isinstance(path, str)
            or not path
            or not os.path.isfile(path)
            or not isinstance(path2, list)
            or not path2
            or not is_description_file(path2)):
        log.critical('compare file with description ... error ... exit | '
                     'illegal input')
        sys.exit(1)

    log.debug('compare file with description ... ongoing | path: %s, '
              'path2: %s',
              path,
              path2)

    file_size = os.path.getsize(path)
    if file_size != get_description_file_size(path2):
        log.debug('compare file with description ... no | file size')
        return False

    file_sha512 = get_description_file_sha512(path2)
    if (file_sha512
            and file_sha512 != util.get_file_sha512(path, bytes_per_read)):
        log.debug('compare file with description ... no | file sha512')
        return False

    log.debug('compare file with description ... yes')
    return True

def count_changes_about_description(path, path2, common, bytes_per_read):
    """统计变化的项的数量，只保留还需要进一步比较的目录项，这里的路径要是列表式的
    """

    if (not isinstance(path, list)
            or not path
            or not os.path.isdir(normalize(path))
            or not isinstance(path2, list)
            or not path2
            or not is_description_dir(path2)
            or not isinstance(common, set)
            or not isinstance(bytes_per_read, int)
            or bytes_per_read <= 0):
        log.critical('count changes about description ... failed ... exit | '
                     'illegal input')
        sys.exit(1)

    log.debug('count changes about description ... ongoing | common: %s',
              common)

    changes = 0
    need_discard = set()
    for entry in common:
        entry_path = os.path.join(normalize(path), entry)
        entry_path2 = join(path2, entry)

        if os.path.isdir(entry_path) and is_description_dir(entry_path2):
            log.debug('count changes about description ... ongoing | '
                      'not hit: %s',
                      entry)
        elif os.path.isfile(entry_path) and is_description_file(entry_path2):
            need_discard.add(entry)

            ok = compare_file_with_description(entry_path, entry_path2,
                                               bytes_per_read)
            if ok:
                log.debug('count changes about description ... ongoing | '
                          'not hit: %s',
                          entry)
            else:
                changes += 1
                log.debug('count changes about description ... ongoing | '
                          'hit: %s', entry)
        elif os.path.isfile(entry_path) and is_description_dir(entry_path2):
            need_discard.add(entry)

            changes += 1
            log.debug('count changes about description ... ongoing | hit: %s',
                      entry)
        elif os.path.isdir(entry_path) and is_description_file(entry_path2):
            need_discard.add(entry)

            changes += 1
            log.debug('count changes about description ... ongoing | hit: %s',
                      entry)
        else:
            log.critical('count changes about description ... failed ... exit '
                         '| should not happen')
            sys.exit(1)

    common.difference_update(need_discard)
    log.debug('count changes about description ... ongoing | common: %s, '
              'need discard: %s',
              common,
              need_discard)

    log.debug('count changes about description ... successed | changes: %d',
              changes)
    return changes

def get_changes_about_description(path, path2, common, bytes_per_read):
    """统计变化的项的数量，只保留还需要进一步比较的目录项，这里的路径要是列表式的
    """

    if (not isinstance(path, list)
            or not path
            or not os.path.isdir(normalize(path))
            or not isinstance(path2, list)
            or not path2
            or not is_description_dir(path2)
            or not isinstance(common, set)
            or not isinstance(bytes_per_read, int)
            or bytes_per_read <= 0):
        log.critical('get changes about description ... failed ... exit | '
                     'illegal input')
        sys.exit(1)

    log.debug('get changes about description ... ongoing | common: %s',
              common)

    changes = []
    need_discard = set()
    for entry in common:
        entry_path = os.path.join(normalize(path), entry)
        entry_path2 = join(path2, entry)

        if os.path.isdir(entry_path) and is_description_dir(entry_path2):
            log.debug('get changes about description ... ongoing | '
                      'not hit: %s',
                      entry)
        elif os.path.isfile(entry_path) and is_description_file(entry_path2):
            need_discard.add(entry)

            ok = compare_file_with_description(entry_path, entry_path2,
                                               bytes_per_read)
            if ok:
                log.debug('get changes about description ... ongoing | '
                          'not hit: %s',
                          entry)
            else:
                changes.append([join(path, entry), join(path2, entry)])
                log.debug('get changes about description ... ongoing | '
                          'hit: %s', entry)
        elif os.path.isfile(entry_path) and is_description_dir(entry_path2):
            need_discard.add(entry)

            changes.append([join(path, entry), join(path2, entry)])
            log.debug('get changes about description ... ongoing | hit: %s',
                      entry)
        elif os.path.isdir(entry_path) and is_description_file(entry_path2):
            need_discard.add(entry)

            changes.append([join(path, entry), join(path2, entry)])
            log.debug('get changes about description ... ongoing | hit: %s',
                      entry)
        else:
            log.critical('get changes about description ... failed ... exit '
                         '| should not happen')
            sys.exit(1)

    common.difference_update(need_discard)
    log.debug('get changes about description ... ongoing | common: %s, '
              'need discard: %s',
              common,
              need_discard)

    log.debug('get changes about description ... successed | changes: %s',
              changes)
    return changes

def compare_with_description_once(path, path2, ignore_hide, bytes_per_read,
                                  add, delete, change):
    """非递归比较本地目录与目录描述信息中的目录，统计差异项数量，这里路径要用列表式的
    """

    if (not isinstance(path, list)
            or not path
            or not os.path.isdir(normalize(path))
            or not isinstance(path2, list)
            or not path2
            or not is_description_dir(path2)
            or not isinstance(bytes_per_read, int)
            or bytes_per_read <= 0
            or not isinstance(add, list)
            or not add
            or not isinstance(add[0], int)
            or not isinstance(delete, list)
            or not delete
            or not isinstance(delete[0], int)
            or not isinstance(change, list)
            or not change
            or not isinstance(change[0], int)):
        log.critical('compare with description once ... failed ... exit | '
                     'illegal input')
        sys.exit(1)

    content = get_dir_regular_content(normalize(path), ignore_hide)
    content2 = get_description_dir_content(path2, ignore_hide)

    log.debug('compare with description once ... ongoing | content: %s, '
              'content2: %s',
              content,
              content2)

    common = content & content2
    add2 = content2 - content
    delete2 = content - content2

    log.debug('compare with description once ... ongoing | common: %s, '
              'add2: %s, delete2: %s',
              common,
              add2,
              delete2)

    add[0] += len(add2)
    delete[0] += len(delete2)
    change[0] += count_changes_about_description(path, path2, common,
                                                 bytes_per_read)

    log.debug('compare with description once ... successed | common: %s',
              common)
    return common

def compare_with_description_once2(path, path2, ignore_hide, bytes_per_read,
                                   add, delete, change):
    """非递归比较本地目录与目录描述信息中的目录，统计差异项数量，这里路径要用列表式的
    """

    if (not isinstance(path, list)
            or not path
            or not os.path.isdir(normalize(path))
            or not isinstance(path2, list)
            or not path2
            or not is_description_dir(path2)
            or not isinstance(bytes_per_read, int)
            or bytes_per_read <= 0
            or not isinstance(add, list)
            or not isinstance(delete, list)
            or not isinstance(change, list)):
        log.critical('compare with description once ... failed ... exit | '
                     'illegal input')
        sys.exit(1)

    content = get_dir_regular_content(normalize(path), ignore_hide)
    content2 = get_description_dir_content(path2, ignore_hide)

    log.debug('compare with description once ... ongoing | content: %s, '
              'content2: %s',
              content,
              content2)

    common = content & content2
    add2 = content2 - content
    delete2 = content - content2

    log.debug('compare with description once ... ongoing | common: %s, '
              'add2: %s, delete2: %s',
              common,
              add2,
              delete2)

    add.extend(extend_entries(path, path2, add2))
    delete.extend(extend_entries(path, path2, delete2))
    change.extend(
        get_changes_about_description(path, path2, common, bytes_per_read))

    log.debug('compare with description once ... successed | common: %s',
              common)
    return common

def compare_with_description(path, description, ignore_hide, bytes_per_read,
                             add, delete, change):
    """递归比较本地目录与目录描述信息，统计差异项数量
    """

    if (not isinstance(path, str)
            or not path
            or not os.path.isdir(path)
            or not isinstance(description, dict)
            or not description
            or not isinstance(bytes_per_read, int)
            or bytes_per_read <= 0
            or not isinstance(add, list)
            or not add
            or not isinstance(add[0], int)
            or not isinstance(delete, list)
            or not delete
            or not isinstance(delete[0], int)
            or not isinstance(change, list)
            or not change
            or not isinstance(change[0], int)):
        log.critical('compare with description ... failed ... exit | illegal '
                     'input')
        sys.exit(1)

    log.debug('compare with description ... ongoing')

    dirs = [[[path], [description]]]
    while dirs:
        log.debug('compare with description ... ongoing | dirs: %s', dirs)

        pair = dirs.pop(0)
        common = compare_with_description_once(pair[0], pair[1], ignore_hide,
                                               bytes_per_read, add, delete,
                                               change)
        dirs += extend_entries(pair[0], pair[1], common)

    log.debug('compare with description ... done')

def compare_with_description2(path, description, ignore_hide, bytes_per_read,
                              add, delete, change):
    """递归比较本地目录与目录描述信息，统计差异项数量
    """

    if (not isinstance(path, str)
            or not path
            or not os.path.isdir(path)
            or not isinstance(description, dict)
            or not description
            or not isinstance(bytes_per_read, int)
            or bytes_per_read <= 0
            or not isinstance(add, list)
            or not isinstance(delete, list)
            or not isinstance(change, list)):
        log.critical('compare with description ... failed ... exit | illegal '
                     'input')
        sys.exit(1)

    log.debug('compare with description ... ongoing')

    dirs = [[[path], [description]]]
    while dirs:
        log.debug('compare with description ... ongoing | dirs: %s', dirs)

        pair = dirs.pop(0)
        common = compare_with_description_once2(pair[0], pair[1], ignore_hide,
                                                bytes_per_read, add, delete,
                                                change)
        dirs += extend_entries(pair[0], pair[1], common)

    log.debug('compare with description ... done')

def encode_path(path):
    """将列表式路径中每段名字编码为base64格式，生成新的列表式路径
    """

    if (not isinstance(path, list)
            or not path
            or not isinstance(path[0], str)
            or not path[0]):
        log.critical('encode path ... failed ... exit | illegal input')
        sys.exit(1)

    path2 = []
    for x in path:
        if isinstance(x, str) and x:
            x = base64.urlsafe_b64encode(x.encode('utf-8')).decode('utf-8')
            path2.append(x)
        else:
            log.critical('encode path ... failed ... exit | invalid input')
            sys.exit(1)

    log.debug('encode path ... successed | path2: %s, path: %s', path2, path)
    return path2

def decode_path(path):
    """为列表式路径中每段名字解base64编码，生成新的列表式路径
    """

    if (not isinstance(path, list)
            or not path
            or not isinstance(path[0], str)
            or not path[0]):
        log.critical('decode path ... failed ... exit | illegal input')
        sys.exit(1)

    path2 = []
    for x in path:
        if isinstance(x, str) and x:
            x = base64.urlsafe_b64decode(x).decode('utf-8')
            path2.append(x)
        else:
            log.critical('decode path ... failed ... exit | invalid input')
            sys.exit(1)

    log.debug('decode path ... successed | path2: %s, path: %s', path2, path)
    return path2

def encode_paths(paths):
    """将列表中的路径对编码为base64格式
    """

    if not isinstance(paths, list):
        log.critical('encode paths ... failed ... exit | illegal input')
        sys.exit(1)

    for x in paths:
        if not isinstance(x, list) or len(x) < 2:
            log.critical('encode paths ... failed ... exit | invalid input')
            sys.exit(1)

        x[0] = encode_path(virtualize(x[0]))
        x[1] = encode_path(virtualize(x[1]))

    log.debug('encode paths ... successed')

def decode_paths(paths):
    """为列表中的路径对解base64编码
    """

    if not isinstance(paths, list):
        log.critical('decode paths ... failed ... exit | illegal input')
        sys.exit(1)

    for x in paths:
        if not isinstance(x, list) or len(x) < 2:
            log.critical('decode paths ... failed ... exit | invalid input')
            sys.exit(1)

        x[0] = decode_path(x[0])
        x[1] = decode_path(x[1])

    log.debug('decode paths ... successed')

def generate_difference(name, add, delete, change):
    """生成差异描述信息，这里路径要是虚化的且base64的
    """

    if (not isinstance(name, str)
            or not name
            or not isinstance(add, list)
            or not isinstance(delete, list)
            or not isinstance(change, list)):
        log.critical('generate difference ... failed ... exit | illegal input')
        sys.exit(1)

    difference = {
        'name': base64.urlsafe_b64encode(name.encode('utf-8')).decode('utf-8'),
        'add': add,
        'delete': delete,
        'change': change
    }

    log.debug('generate difference ... successed')
    return difference

def restore_difference(difference):
    """恢复difference内容

    恢复difference内容，主要是解base64编码
    """

    if not isinstance(difference, dict) or not difference:
        log.critical('restore difference ... failed ... exit | illegal input')
        sys.exit(1)

    if not {'name', 'add', 'delete', 'change'} <= difference.keys():
        log.critical('restore difference ... failed ... exit | invalid input')
        sys.exit(1)

    name = difference['name']
    add = difference['add']
    delete = difference['delete']
    change = difference['change']

    if not isinstance(name, str) or not name:
        log.critical('restore difference ... failed ... exit | invalid input')
        sys.exit(1)

    difference['name'] = base64.urlsafe_b64decode(name).decode('utf-8')

    decode_paths(add)
    decode_paths(delete)
    decode_paths(change)

    log.debug('restore difference ... successed | difference: %s', difference)
    return difference

def get_difference_name(difference):
    """获取差异描述信息的指代名称
    """

    if (not isinstance(difference, dict)
            or not difference
            or 'name' not in difference.keys()):
        log.critical('get difference name ... failed ... exit | illegal input')
        sys.exit(1)

    name = difference['name']
    if not isinstance(name, str) or not name:
        log.critical('get difference name ... failed ... exit | invalid input')
        sys.exit(1)

    return name

def get_difference_paths(difference, category):
    """获取差异描述信息的路径对列表
    """

    if (not isinstance(difference, dict)
            or not difference
            or not isinstance(category, str)
            or not category
            or category not in difference.keys()):
        log.critical('get difference paths ... failed ... exit | illegal input')
        sys.exit(1)

    paths = difference[category]
    if not isinstance(paths, list):
        log.critical('get difference paths ... failed ... exit | invalid input')
        sys.exit(1)

    return paths

def export_paths(path, path2, paths, ignore_hide, failed=None):
    """结合本地目录与差异描述信息的路径对导出文件
    """

    if (not isinstance(path, str)
            or not path
            or not os.path.isdir(path)
            or not isinstance(path2, str)
            or not path2
            or not isinstance(paths, list)):
        log.critical('export paths ... failed ... exit | illegal input')
        sys.exit(1)

    log.info('export paths ... ongoing')

    for x in paths:
        src = normalize(realize(path, x[1]))
        dest = normalize(realize(path2, x[1]))

        if os.path.isfile(src):
            prompt_status2('Export file', 'ongoing', src)
            ok = copy_regular_file(src, dest)
            prompt_assert(ok, 'Export file', src, False)

            if not ok and isinstance(failed, list):
                failed.append((src, dest))
        elif os.path.isdir(src):
            prompt_status2('Export dir', 'ongoing', src)
            ok = copy_regular_dir(src, dest, ignore_hide, failed)
            prompt_assert(ok, 'Export dir', src, False)
        else:
            log.critical('export paths ... failed ... exit | should not happen')
            sys.exit(1)

    log.info('export paths ... done')

def realize_paths(path, path2, paths):
    """实化列表中的路径对
    """

    if (not isinstance(path, str)
            or not path
            or not os.path.isdir(path)
            or not isinstance(path2, str)
            or not path2
            or not os.path.isdir(path2)
            or not isinstance(paths, list)):
        log.critical('realize paths ... failed ... exit | illegal input')
        sys.exit(1)

    for x in paths:
        if not isinstance(x, list) or len(x) < 2:
            log.critical('realize paths ... failed ... exit | invalid input')
            sys.exit(1)

        x[0] = realize(path, x[0])
        x[1] = realize(path2, x[1])

    log.debug('realize paths ... successed')

def compare_files_in_descriptions(path, path2):
    """非递归的比较本目录描述信息中的文件，路径是列表式的
    """

    if (not isinstance(path, list)
            or not path
            or not is_description_file(path)
            or not isinstance(path2, list)
            or not path2
            or not is_description_file(path2)):
        log.critical('compare files in descriptions ... error ... exit | '
                     'illegal input')
        sys.exit(1)

    file_size = get_description_file_size(path)
    if file_size != get_description_file_size(path2):
        log.debug('compare files in descriptions ... no | file size')
        return False

    file_sha512 = get_description_file_sha512(path)
    file_sha5122 = get_description_file_sha512(path2)
    if (file_sha512
            and file_sha5122
            and file_sha512 != file_sha5122):
        log.debug('compare files in descriptions ... no | file sha512')
        return False

    log.debug('compare files in descriptions ... yes')
    return True

def count_changes_between_descriptions(path, path2, common):
    """统计变化的项的数量，只保留还需要进一步比较的目录项，这里的路径要是列表式的
    """

    if (not isinstance(path, list)
            or not path
            or not is_description_dir(path)
            or not isinstance(path2, list)
            or not path2
            or not is_description_dir(path2)
            or not isinstance(common, set)):
        log.critical('count changes between descriptions ... failed ... exit | '
                     'illegal input')
        sys.exit(1)

    log.debug('count changes between descriptions ... ongoing | common: %s',
              common)

    changes = 0
    need_discard = set()
    for entry in common:
        entry_path = join(path, entry)
        entry_path2 = join(path2, entry)

        if is_description_dir(entry_path) and is_description_dir(entry_path2):
            log.debug('count changes between descriptions ... ongoing | '
                      'not hit: %s',
                      entry)
        elif (is_description_file(entry_path)
                and is_description_file(entry_path2)):
            need_discard.add(entry)

            ok = compare_files_in_descriptions(entry_path, entry_path2)
            if ok:
                log.debug('count changes between descriptions ... ongoing | '
                          'not hit: %s',
                          entry)
            else:
                changes += 1
                log.debug('count changes between descriptions ... ongoing | '
                          'hit: %s', entry)
        elif (is_description_file(entry_path)
                and is_description_dir(entry_path2)):
            need_discard.add(entry)

            changes += 1
            log.debug('count changes between descriptions ... ongoing | '
                      'hit: %s',
                      entry)
        elif (is_description_dir(entry_path)
                and is_description_file(entry_path2)):
            need_discard.add(entry)

            changes += 1
            log.debug('count changes between descriptions ... ongoing | '
                      'hit: %s',
                      entry)
        else:
            log.critical('count changes between descriptions ... failed ... '
                         'exit | should not happen')
            sys.exit(1)

    common.difference_update(need_discard)
    log.debug('count changes between descriptions ... ongoing | common: %s, '
              'need discard: %s',
              common,
              need_discard)

    log.debug('count changes between descriptions ... successed | changes: %d',
              changes)
    return changes

def get_changes_between_descriptions(path, path2, common):
    """统计变化的项的数量，只保留还需要进一步比较的目录项，这里的路径要是列表式的
    """

    if (not isinstance(path, list)
            or not path
            or not is_description_dir(path)
            or not isinstance(path2, list)
            or not path2
            or not is_description_dir(path2)
            or not isinstance(common, set)):
        log.critical('get changes between descriptions ... failed ... exit | '
                     'illegal input')
        sys.exit(1)

    log.debug('get changes between descriptions ... ongoing | common: %s',
              common)

    changes = []
    need_discard = set()
    for entry in common:
        entry_path = join(path, entry)
        entry_path2 = join(path2, entry)

        if is_description_dir(entry_path) and is_description_dir(entry_path2):
            log.debug('get changes between descriptions ... ongoing | '
                      'not hit: %s',
                      entry)
        elif (is_description_file(entry_path)
                and is_description_file(entry_path2)):
            need_discard.add(entry)

            ok = compare_files_in_descriptions(entry_path, entry_path2)
            if ok:
                log.debug('get changes between descriptions ... ongoing | '
                          'not hit: %s',
                          entry)
            else:
                changes.append([entry_path, entry_path2])
                log.debug('get changes between descriptions ... ongoing | '
                          'hit: %s', entry)
        elif (is_description_file(entry_path)
                and is_description_dir(entry_path2)):
            need_discard.add(entry)

            changes.append([entry_path, entry_path2])
            log.debug('get changes between descriptions ... ongoing | hit: %s',
                      entry)
        elif (is_description_dir(entry_path)
                and is_description_file(entry_path2)):
            need_discard.add(entry)

            changes.append([entry_path, entry_path2])
            log.debug('get changes between descriptions ... ongoing | hit: %s',
                      entry)
        else:
            log.critical('get changes between descriptions ... failed ... exit '
                         '| should not happen')
            sys.exit(1)

    common.difference_update(need_discard)
    log.debug('get changes between descriptions ... ongoing | common: %s, '
              'need discard: %s',
              common,
              need_discard)

    log.debug('get changes between descriptions ... successed | changes: %s',
              changes)
    return changes

def compare_descriptions_once(path, path2, ignore_hide, add, delete, change):
    """非递归比较目录描述信息，统计差异项数量，这里路径要用列表式的
    """

    if (not isinstance(path, list)
            or not path
            or not is_description_dir(path)
            or not isinstance(path2, list)
            or not path2
            or not is_description_dir(path2)
            or not isinstance(add, list)
            or not add
            or not isinstance(add[0], int)
            or not isinstance(delete, list)
            or not delete
            or not isinstance(delete[0], int)
            or not isinstance(change, list)
            or not change
            or not isinstance(change[0], int)):
        log.critical('compare descriptions once ... failed ... exit | illegal '
                     'input')
        sys.exit(1)

    content = get_description_dir_content(path, ignore_hide)
    content2 = get_description_dir_content(path2, ignore_hide)

    log.debug('compare descriptions once ... ongoing | content: %s, '
              'content2: %s',
              content,
              content2)

    common = content & content2
    add2 = content2 - content
    delete2 = content - content2

    log.debug('compare descriptions once ... ongoing | common: %s, add2: %s, '
              'delete2: %s',
              common,
              add2,
              delete2)

    add[0] += len(add2)
    delete[0] += len(delete2)
    change[0] += count_changes_between_descriptions(path, path2, common)

    log.debug('compare descriptions once ... successed | common: %s', common)
    return common

def compare_descriptions_once2(path, path2, ignore_hide, add, delete, change):
    """非递归比较目录描述信息，统计差异项数量，这里路径要用列表式的
    """

    if (not isinstance(path, list)
            or not path
            or not is_description_dir(path)
            or not isinstance(path2, list)
            or not path2
            or not is_description_dir(path2)
            or not isinstance(add, list)
            or not isinstance(delete, list)
            or not isinstance(change, list)):
        log.critical('compare descriptions once ... failed ... exit | illegal '
                     'input')
        sys.exit(1)

    content = get_description_dir_content(path, ignore_hide)
    content2 = get_description_dir_content(path2, ignore_hide)

    log.debug('compare descriptions once ... ongoing | content: %s, '
              'content2: %s',
              content,
              content2)

    common = content & content2
    add2 = content2 - content
    delete2 = content - content2

    log.debug('compare descriptions once ... ongoing | common: %s, add2: %s, '
              'delete2: %s',
              common,
              add2,
              delete2)

    add.extend(extend_entries(path, path2, add2))
    delete.extend(extend_entries(path, path2, delete2))
    change.extend(get_changes_between_descriptions(path, path2, common))

    log.debug('compare descriptions once ... successed | common: %s', common)
    return common

def compare_descriptions(description, description2, ignore_hide, add, delete,
                         change):
    """递归比较目录描述信息，统计差异项数量
    """

    if (not isinstance(description, dict)
            or not description
            or not isinstance(description2, dict)
            or not description2
            or not isinstance(add, list)
            or not add
            or not isinstance(add[0], int)
            or not isinstance(delete, list)
            or not delete
            or not isinstance(delete[0], int)
            or not isinstance(change, list)
            or not change
            or not isinstance(change[0], int)):
        log.critical('compare descriptions ... failed ... exit | illegal input')
        sys.exit(1)

    dirs = [[[description], [description2]]]
    while dirs:
        log.debug('compare descriptions ... ongoing | dirs: %s', dirs)

        pair = dirs.pop(0)
        common = compare_descriptions_once(pair[0], pair[1], ignore_hide,
                                           add, delete, change)
        dirs += extend_entries(pair[0], pair[1], common)

    log.debug('compare descriptions ... done')

def compare_descriptions2(description, description2, ignore_hide, add, delete,
                          change):
    """递归比较目录描述信息，统计差异项数量
    """

    if (not isinstance(description, dict)
            or not description
            or not isinstance(description2, dict)
            or not description2
            or not isinstance(add, list)
            or not isinstance(delete, list)
            or not isinstance(change, list)):
        log.critical('compare descriptions ... failed ... exit | illegal input')
        sys.exit(1)

    dirs = [[[description], [description2]]]
    while dirs:
        log.debug('compare descriptions ... ongoing | dirs: %s', dirs)

        pair = dirs.pop(0)
        common = compare_descriptions_once2(pair[0], pair[1], ignore_hide,
                                            add, delete, change)
        dirs += extend_entries(pair[0], pair[1], common)

    log.debug('compare descriptions ... done')

def prompt_status(action, status, path, path2):
    """输出规范提示信息
    """

    if (not isinstance(action, str)
            or not action
            or not isinstance(status, str)
            or not status
            or not isinstance(path, str)
            or not path
            or not isinstance(path2, str)
            or not path2):
        log.critical('prompt status ... failed ... exit | illegal input')
        sys.exit(1)

    log.info('prompt status ... ongoing | action: %s, status: %s, path: %s, '
             'path2: %s',
             action,
             status,
             path,
             path2)

    now = datetime.datetime.now()

    prompt = ('[%s]%s ... %s'
              % (now.strftime('%Y-%m-%d %H:%M:%S'), action, status))
    print(prompt)
    print(path)
    print(path2)

def prompt_status2(action, status, path):
    """输出规范提示信息
    """

    if (not isinstance(action, str)
            or not action
            or not isinstance(status, str)
            or not status
            or not isinstance(path, str)
            or not path):
        log.critical('prompt status ... failed ... exit | illegal input')
        sys.exit(1)

    log.info('prompt status ... ongoing | action: %s, status: %s, path: %s',
             action,
             status,
             path)

    now = datetime.datetime.now()

    prompt = ('[%s]%s ... %s'
              % (now.strftime('%Y-%m-%d %H:%M:%S'), action, status))
    print(prompt)
    print(path)

def prompt_assert(yes, action, path, exit=True):
    """输出规范提示信息
    """

    if (not isinstance(action, str)
            or not action
            or not isinstance(path, str)
            or not path):
        log.critical('prompt assert ... failed ... exit | illegal input')
        sys.exit(1)

    log.info('prompt assert ... ongoing | yes: %s, action: %s, path: %s',
             bool(yes),
             action,
             path)

    now = datetime.datetime.now()

    if yes:
        prompt = ('[%s]%s ... successed'
                  % (now.strftime('%Y-%m-%d %H:%M:%S'), action))
        print(prompt)
        print(path)
    else:
        prompt = ('[%s]%s ... failed'
                  % (now.strftime('%Y-%m-%d %H:%M:%S'), action))
        print(prompt)
        print(path)

        if exit:
            log.critical('prompt assert ... failed ... exit')
            sys.exit(1)

def prompt_batch(action, add, delete, change, path, path2):
    """输出批处理的规范提示信息
    """

    if (not isinstance(action, str)
            or not action
            or not isinstance(add, int)
            or not isinstance(delete, int)
            or not isinstance(change, int)
            or not isinstance(path, str)
            or not path
            or not isinstance(path2, str)
            or not path2):
        log.critical('prompt batch ... failed ... exit | illegal input')
        sys.exit(1)

    log.info('prompt batch ... ongoing | action: %s, add: %d, delete: %d, '
             'change: %d, path: %s, path2: %s',
             action,
             add,
             delete,
             change,
             path,
             path2)

    now = datetime.datetime.now()

    prompt = ('[%s]%s: add %d, delete %d, change %d'
              % (now.strftime('%Y-%m-%d %H:%M:%S'),
                 action,
                 add,
                 delete,
                 change))
    print(prompt)
    print(path)
    print(path2)

def prompt_batch2(action, add, delete, change, path, path2):
    """输出批处理的规范提示信息
    """

    if (not isinstance(action, str)
            or not action
            or not isinstance(add, list)
            or not isinstance(delete, list)
            or not isinstance(change, list)
            or not isinstance(path, str)
            or not path
            or not isinstance(path2, str)
            or not path2):
        log.critical('prompt batch ... failed ... exit | illegal input')
        sys.exit(1)

    log.info('prompt batch ... ongoing | action: %s, add: %d, delete: %d, '
             'change: %d, path: %s, path2: %s',
             action,
             len(add),
             len(delete),
             len(change),
             path,
             path2)

    now = datetime.datetime.now()

    if add or delete or change:
        prompt = '[%s]Report:' % now.strftime('%Y-%m-%d %H:%M:%S')
        print(prompt)

    if add:
        dirs, files = classify_paths(add, False)

        if dirs:
            log.info('prompt batch ... ongoing | add dir')
            print('Add dir:')
            for info in dirs:
                if isinstance(info, str):
                    log.info('prompt batch ... ongoing | %s', info)
                    print(info)

        if files:
            log.info('prompt batch ... ongoing | add file')
            print('Add file:')
            for info in files:
                if isinstance(info, str):
                    log.info('prompt batch ... ongoing | %s', info)
                    print(info)

    if delete:
        dirs, files = classify_paths(delete)

        if dirs:
            log.info('prompt batch ... ongoing | delete dir')
            print('Delete dir:')
            for info in dirs:
                if isinstance(info, str):
                    log.info('prompt batch ... ongoing | %s', info)
                    print(info)

        if files:
            log.info('prompt batch ... ongoing | delete file')
            print('Delete file:')
            for info in files:
                if isinstance(info, str):
                    log.info('prompt batch ... ongoing | %s', info)
                    print(info)

    if change:
        type_change, content_change = classify_change(change)

        if type_change:
            log.info('prompt batch ... ongoing | type change')
            print('Type change:')
            for info in type_change:
                if isinstance(info, str):
                    log.info('prompt batch ... ongoing | %s', info)
                    print(info)

        if content_change:
            log.info('prompt batch ... ongoing | content change')
            print('Content change:')
            for info in content_change:
                if isinstance(info, str):
                    log.info('prompt batch ... ongoing | %s', info)
                    print(info)

    prompt = ('[%s]%s: add %d, delete %d, change %d'
              % (now.strftime('%Y-%m-%d %H:%M:%S'),
                 action,
                 len(add),
                 len(delete),
                 len(change)))
    print(prompt)
    print(path)
    print(path2)

def prompt_batch3(action, add, add_failed, delete, delete_failed, change,
                  change_delete_failed, change_copy_failed, path, path2):
    """输出批处理的规范提示信息
    """

    if (not isinstance(action, str)
            or not action
            or not isinstance(add, list)
            or not isinstance(add_failed, list)
            or not isinstance(delete, list)
            or not isinstance(delete_failed, list)
            or not isinstance(change, list)
            or not isinstance(change_delete_failed, list)
            or not isinstance(change_copy_failed, list)
            or not isinstance(path, str)
            or not path
            or not isinstance(path2, str)
            or not path2):
        log.critical('prompt batch ... failed ... exit | illegal input')
        sys.exit(1)

    log.info('prompt batch ... ongoing | action: %s, add: %d, add_failed: %d, '
             'delete: %d, delete_failed: %d, change: %d, '
             'change_delete_failed: %d, change_copy_failed: %d '
             'path: %s, path2: %s',
             action,
             len(add),
             len(add_failed),
             len(delete),
             len(delete_failed),
             len(change),
             len(change_delete_failed),
             len(change_copy_failed),
             path,
             path2)

    now = datetime.datetime.now()

    if (add_failed
            or delete_failed
            or change_delete_failed
            or change_copy_failed):
        prompt = '[%s]Report:' % now.strftime('%Y-%m-%d %H:%M:%S')
        print(prompt)

    if add_failed:
        log.info('prompt batch ... ongoing | add failed')
        print('Add %d:' % len(add))
        print('Copying failed:')
        for i in range(len(add_failed)):
            info = add_failed[i]

            if (isinstance(info, tuple)
                    and len(info) == 2
                    and isinstance(info[0], str)
                    and isinstance(info[1], str)):
                log.info('prompt batch ... ongoing | %s to %s', info[0],
                         info[1])

                if i:
                    print('')

                print(info[0])
                print(info[1])

    if delete_failed:
        log.info('prompt batch ... ongoing | delete failed')
        print('Delete %d:' % len(delete))
        print('Deleting failed:')
        for info in delete_failed:
            if isinstance(info, str):
                log.info('prompt batch ... ongoing | %s', info)
                print(info)

    if change_delete_failed or change_copy_failed:
        print('Change %d:' % len(change))

    if change_delete_failed:
        log.info('prompt batch ... ongoing | change delete failed')
        print('Deleting failed:')
        for info in change_delete_failed:
            if isinstance(info, str):
                log.info('prompt batch ... ongoing | %s', info)
                print(info)

    if change_copy_failed:
        log.info('prompt batch ... ongoing | change copy failed')
        print('Copying failed:')
        for i in range(len(change_copy_failed)):
            info = change_copy_failed[i]

            if (isinstance(info, tuple)
                    and len(info) == 2
                    and isinstance(info[0], str)
                    and isinstance(info[1], str)):
                log.info('prompt batch ... ongoing | %s to %s', info[0],
                         info[1])

                if i:
                    print('')

                print(info[0])
                print(info[1])

    prompt = ('[%s]%s: add %d(copying failed %d), '
              'delete %d(deleting failed %d), '
              'change %d(deleting failed %d, copying failed %d)'
              % (now.strftime('%Y-%m-%d %H:%M:%S'),
              action,
              len(add),
              len(add_failed),
              len(delete),
              len(delete_failed),
              len(change),
              len(change_delete_failed),
              len(change_copy_failed)))
    print(prompt)
    print(path)
    print(path2)

def prompt_batch4(action, add, add_failed, change, change_failed, path, path2):
    """输出批处理的规范提示信息
    """

    if (not isinstance(action, str)
            or not action
            or not isinstance(add, list)
            or not isinstance(add_failed, list)
            or not isinstance(change, list)
            or not isinstance(change_failed, list)
            or not isinstance(path, str)
            or not path
            or not isinstance(path2, str)
            or not path2):
        log.critical('prompt batch ... failed ... exit | illegal input')
        sys.exit(1)

    log.info('prompt batch ... ongoing | action: %s, add: %d, add_failed: %d, '
             'change: %d, change_failed: %d, path: %s, path2: %s',
             action,
             len(add),
             len(add_failed),
             len(change),
             len(change_failed),
             path,
             path2)

    now = datetime.datetime.now()

    if add_failed or change_failed:
        prompt = '[%s]Report:' % now.strftime('%Y-%m-%d %H:%M:%S')
        print(prompt)

    if add_failed:
        log.info('prompt batch ... ongoing | export add failed')
        print('Export adding %d:' % len(add))
        print('Copying failed:')
        for i in range(len(add_failed)):
            info = add_failed[i]

            if (isinstance(info, tuple)
                    and len(info) == 2
                    and isinstance(info[0], str)
                    and isinstance(info[1], str)):
                log.info('prompt batch ... ongoing | %s to %s', info[0],
                         info[1])

                if i:
                    print('')

                print(info[0])
                print(info[1])

    if change_failed:
        log.info('prompt batch ... ongoing | export change failed')
        print('Export changing %d:' % len(change))
        print('Copying failed:')
        for i in range(len(change_failed)):
            info = change_failed[i]

            if (isinstance(info, tuple)
                    and len(info) == 2
                    and isinstance(info[0], str)
                    and isinstance(info[1], str)):
                log.info('prompt batch ... ongoing | %s to %s', info[0],
                         info[1])

                if i:
                    print('')

                print(info[0])
                print(info[1])

    prompt = ('[%s]%s: Export adding %d(copying failed %d), '
              'export changing %d(copying failed %d)'
              % (now.strftime('%Y-%m-%d %H:%M:%S'),
              action,
              len(add),
              len(add_failed),
              len(change),
              len(change_failed)))
    print(prompt)
    print(path)
    print(path2)

def local_list():
    log.info('local list ... ongoing')

    add = [0] if args.count else []
    delete = [0] if args.count else []
    change = [0] if args.count else []

    prompt_status('Compare the following dirs', 'ongoing', args.dir, args.dir2)

    if args.count:
        compare_dirs(args.dir,
                     args.dir2,
                     not args.expose,
                     args.ignore_file_content,
                     args.mb_per_read * 1024 * 1024,
                     add,
                     delete,
                     change)
        prompt_batch('Compare the following dirs', add[0], delete[0], change[0],
                     args.dir, args.dir2)
    else:
        compare_dirs2(args.dir,
                      args.dir2,
                      not args.expose,
                      args.ignore_file_content,
                      args.mb_per_read * 1024 * 1024,
                      add,
                      delete,
                      change)
        prompt_batch2('Compare the following dirs', add, delete, change,
                      args.dir, args.dir2)

    log.info('local list ... done')

def local_update():
    log.info('local update ... ongoing')

    add = []
    delete = []
    change = []

    prompt_status('Compare the following dirs', 'ongoing', args.dir, args.dir2)
    compare_dirs2(args.dir,
                  args.dir2,
                  not args.expose,
                  args.ignore_file_content,
                  args.mb_per_read * 1024 * 1024,
                  add,
                  delete,
                  change)

    if args.count:
        prompt_batch('Compare the following dirs', len(add), len(delete),
                     len(change), args.dir, args.dir2)
    else:
        prompt_batch2('Compare the following dirs', add, delete, change,
                      args.dir, args.dir2)

    if (not (args.add and add)
            and not (args.delete and delete)
            and not (args.change and change)):
        log.info('local update ... done')
        return

    prompt_status('Update the 1st using the 2nd', 'ongoing', args.dir,
                  args.dir2)

    add_failed = []
    if args.add and add:
        add_entries(add, not args.expose, add_failed)

    delete_failed = []
    if args.delete and delete:
        delete_entries(delete, delete_failed)

    change_delete_failed = []
    change_copy_failed = []
    if args.change and change:
        change_entries(change, not args.expose, change_delete_failed,
                       change_copy_failed)

    prompt_batch3('Update the 1st using the 2nd',
                  add if args.add and add else [],
                  add_failed,
                  delete if args.delete and delete else [],
                  delete_failed,
                  change if args.change and change else [],
                  change_delete_failed,
                  change_copy_failed,
                  args.dir,
                  args.dir2)

    log.info('local update ... done')

def local():
    log.info('local ... ongoing')

    if not args.add and not args.delete and not args.change:
        local_list()
    else:
        local_update()

    log.info('local ... done')

def remote_list():
    log.info('remote list ... ongoing')

    prompt_status2('Get description', 'ongoing', args.description)
    description = util.get_json(args.description, restore_description)
    prompt_assert(description, 'Get description', args.description)

    add = [0] if args.count else []
    delete = [0] if args.count else []
    change = [0] if args.count else []

    prompt_status('Compare the following things', 'ongoing', args.dir,
                  args.description)

    if args.count:
        compare_with_description(args.dir,
                                 description,
                                 not args.expose,
                                 args.mb_per_read * 1024 * 1024,
                                 add,
                                 delete,
                                 change)
        prompt_batch('Compare the following things', add[0], delete[0],
                     change[0], args.dir, args.description)
    else:
        compare_with_description2(args.dir,
                                  description,
                                  not args.expose,
                                  args.mb_per_read * 1024 * 1024,
                                  add,
                                  delete,
                                  change)
        prompt_batch2('Compare the following things', add, delete, change,
                      args.dir, args.description)

    log.info('remote list ... done')

def remote_export():
    log.info('remote export ... ongoing')

    path = os.path.join(args.out_dir,
                        '%s%s' % (args.name, difference_extension))
    util.prompt_false(not os.path.exists(path) or os.path.isfile(path),
                      'Export difference ... failed')

    prompt_status2('Get description', 'ongoing', args.description)
    description = util.get_json(args.description, restore_description)
    prompt_assert(description, 'Get description', args.description)

    add = []
    delete = []
    change = []

    prompt_status('Compare the following things', 'ongoing', args.dir,
                  args.description)
    compare_with_description2(args.dir,
                              description,
                              not args.expose,
                              args.mb_per_read * 1024 * 1024,
                              add,
                              delete,
                              change)

    if args.count:
        prompt_batch('Compare the following things', len(add), len(delete),
                     len(change), args.dir, args.description)
    else:
        prompt_batch2('Compare the following things', add, delete, change,
                      args.dir, args.description)

    encode_paths(add)
    encode_paths(delete)
    encode_paths(change)

    prompt_status2('Export difference', 'ongoing', path)
    difference = generate_difference(args.name, add, delete, change)
    ok = util.save_to_json(path, difference, args.compact)
    prompt_assert(ok, 'Export difference', path)

    log.info('remote export ... done')

def remote():
    log.info('remote ... ongoing')

    if args.difference:
        remote_export()
    else:
        remote_list()

    log.info('remote ... done')

def export_description():
    log.info('export description ... ongoing')

    path = os.path.join(args.out_dir,
                        '%s%s' % (args.name, description_extension))
    util.prompt_false(not os.path.exists(path) or os.path.isfile(path),
                      'Export description ... failed')

    prompt_status2('Export description', 'ongoing', path)
    description = generate_dir_description(args.dir,
                                           args.name,
                                           not args.expose,
                                           args.disable_digest,
                                           args.mb_per_read * 1024 * 1024)
    ok = util.save_to_json(path, description, args.compact)
    prompt_assert(ok, 'Export description', path)

    log.info('export description ... done')

def export_differences():
    log.info('export differences ... ongoing')

    prompt_status2('Get difference', 'ongoing', args.difference)
    difference = util.get_json(args.difference, restore_difference)
    prompt_assert(difference, 'Get difference', args.difference)

    path = os.path.join(args.out_dir, get_difference_name(difference))
    util.prompt_false(not os.path.exists(path) or os.path.isdir(path),
                      'Export differences ... failed')

    add = get_difference_paths(difference, 'add')
    change = get_difference_paths(difference, 'change')

    prompt_status('Export differences from the 1st to the 2nd', 'ongoing',
                  args.dir, path)

    add_failed = []
    if add:
        export_paths(args.dir, path, add, not args.expose, add_failed)

    change_failed = []
    if change:
        export_paths(args.dir, path, change, not args.expose, change_failed)

    prompt_batch4('Export differences from the 1st to the 2nd',
                  add,
                  add_failed,
                  change,
                  change_failed,
                  args.dir,
                  path)

    log.info('export differences ... done')

def export():
    log.info('export ... ongoing')

    if args.differences:
        export_differences()
    else:
        export_description()

    log.info('export ... done')

def import_():
    log.info('import ... ongoing')

    prompt_status2('Get difference', 'ongoing', args.difference)
    difference = util.get_json(args.difference, restore_difference)
    prompt_assert(difference, 'Get difference', args.difference)

    add = get_difference_paths(difference, 'add')
    delete = get_difference_paths(difference, 'delete')
    change = get_difference_paths(difference, 'change')

    realize_paths(args.dir, args.dir2, add)
    realize_paths(args.dir, args.dir2, delete)
    realize_paths(args.dir, args.dir2, change)

    prompt_status('Update the 1st using the 2nd', 'ongoing', args.dir,
                  args.dir2)

    add_failed = []
    if args.add and add:
        add_entries(add, not args.expose, add_failed)

    delete_failed = []
    if args.delete and delete:
        delete_entries(delete, delete_failed)

    change_delete_failed = []
    change_copy_failed = []
    if args.change and change:
        change_entries(change, not args.expose, change_delete_failed,
                       change_copy_failed)

    prompt_batch3('Update the 1st using the 2nd',
                  add if args.add and add else [],
                  add_failed,
                  delete if args.delete and delete else [],
                  delete_failed,
                  change if args.change and change else [],
                  change_delete_failed,
                  change_copy_failed,
                  args.dir,
                  args.dir2)

    log.info('import ... done')

def history():
    log.info('history ... ongoing')

    prompt_status2('Get description', 'ongoing', args.description)
    description = util.get_json(args.description, restore_description)
    prompt_assert(description, 'Get description', args.description)

    prompt_status2('Get description2', 'ongoing', args.description2)
    description2 = util.get_json(args.description2, restore_description)
    prompt_assert(description2, 'Get description2', args.description2)

    add = [0] if args.count else []
    delete = [0] if args.count else []
    change = [0] if args.count else []

    prompt_status('Compare the following descriptions', 'ongoing',
                  args.description, args.description2)

    if args.count:
        compare_descriptions(description,
                             description2,
                             not args.expose,
                             add,
                             delete,
                             change)
        prompt_batch('Compare the following descriptions', add[0], delete[0],
                     change[0], args.description, args.description2)
    else:
        compare_descriptions2(description,
                              description2,
                              not args.expose,
                              add,
                              delete,
                              change)
        prompt_batch2('Compare the following descriptions', add, delete, change,
                      args.description, args.description2)

    log.info('history ... done')

def check_args(args, subcommand):
    if args.subcommand == 'remote':
        if args.difference:
            if not args.out_dir:
                subcommand.error('expect arguement -o/--out_dir')

            if not args.name:
                subcommand.error('expect arguement -n/--name')

    if args.subcommand == 'export':
        if args.differences:
            if not args.difference:
                subcommand.error('expect arguement -f/--difference')
        else:
            if not args.name:
                subcommand.error('expect arguement -n/--name')

    return args

def parse_command_line():
    parser = argparse.ArgumentParser(
        prog=__product__,
        description='比较本地目录并进行差异管理或者通过目录描述文件与远程目录进行间接比较'
                    '并进行差异管理')

    parser.add_argument('-v', '--version',
                        action='version',
                        version='%(prog)s ' + __version__)

    subparsers = parser.add_subparsers(
        title='subcommands',
        required=True,
        dest='subcommand',
        description='子命令集，由相应的子命令完成相应的功能')

    common = argparse.ArgumentParser(add_help=False)
    common.add_argument('-e', '--expose',
                        action='store_true',
                        help='处理目录时，默认会忽略以“.”开头的项，有此选项则不忽略')

    common2 = argparse.ArgumentParser(add_help=False)
    common2.add_argument('-c', '--count',
                         action='store_true',
                         help='列出差异时，有此选项则只统计数量')

    common3 = argparse.ArgumentParser(add_help=False)
    common3.add_argument('-m', '--mb_per_read',
                         type=util.arg_type_mb_per_read,
                         default=4,
                         help='每次操作读文件数据的大小，单位是M，最小为1，最大为1024，'
                              '默认是4')

    common4 = argparse.ArgumentParser(add_help=False)
    common4.add_argument('-p', '--compact',
                         action='store_true',
                         help='以紧凑格式生成文件')

    common5 = argparse.ArgumentParser(add_help=False)
    common5.add_argument('-a', '--add',
                         action='store_true',
                         help='有此选项会向基准目录添加应添加的内容')
    common5.add_argument('-d', '--delete',
                         action='store_true',
                         help='有此选项会删除基准目录中应删除的内容')
    common5.add_argument('-g', '--change',
                         action='store_true',
                         help='有此选项会替换基准目录中应替换的内容')

    # local子命令，比较本地两个目录并进行差异管理
    local = subparsers.add_parser(
        'local',
        help='比较本地两个目录并进行差异管理',
        parents=[
            util.arg_log(),
            util.arg_log_identifier(),
            util.arg_shutdown(),
            common,
            common2,
            common3,
            common5
        ]
    )
    local.add_argument('dir',
                       type=util.arg_type_dir,
                       help='比较的基准目录的完整路径')
    local.add_argument('dir2',
                       type=util.arg_type_dir,
                       help='与基准目录进行比较的对比目录的完整路径')
    local.add_argument('-f', '--ignore_file_content',
                       action='store_true',
                       help='默认会比较文件内容，有此选项会忽略文件内容比较')

    # remote子命令，比较本地目录与目录描述文件以列出差异或者生成差异描述文件
    remote = subparsers.add_parser(
        'remote',
        help='比较本地目录与目录描述文件以列出差异或者生成差异描述文件',
        parents=[
            util.arg_log(),
            util.arg_log_identifier(),
            util.arg_shutdown(),
            common,
            common2,
            common3,
            common4
        ]
    )
    remote.add_argument('dir',
                        type=util.arg_type_dir,
                        help='比较的基准目录的完整路径')
    remote.add_argument('description',
                        type=util.arg_type_file2,
                        help='与基准目录进行比较的目录描述文件，可提供本地文件的完整路'
                             '径，也可提供一个HTTP/HTTPS地址')
    remote.add_argument('-f', '--difference',
                        action='store_true',
                        help='比较后默认只列出差异，有此选项会生成差异描述文件')
    remote.add_argument('-o', '--out_dir',
                        type=util.arg_type_dir,
                        help='用来存放导出内容的目录的完整路径，difference选项需要此'
                             '选项')
    remote.add_argument('-n', '--name',
                        type=util.arg_type_filename,
                        help='差异描述文件对应的差异的指代名称，difference选项需要此'
                             '选项')

    # export子命令，生成目录描述文件或者根据差异描述文件复制出差异部分
    export = subparsers.add_parser(
        'export',
        help='生成目录描述文件或者根据差异描述文件复制出目录的差异部分',
        parents=[
            util.arg_log(),
            util.arg_log_identifier(),
            util.arg_shutdown(),
            common,
            common3,
            common4
        ]
    )
    export.add_argument('dir',
                        type=util.arg_type_dir,
                        help='待生成目录描述文件的目录或者待复制出差异部分的目录的完整'
                             '路径')
    export.add_argument('out_dir',
                        type=util.arg_type_dir,
                        help='用来存放导出内容的目录的完整路径')
    export.add_argument('-n', '--name',
                        type=util.arg_type_filename,
                        help='目录描述文件对应的目录的指代名称')
    export.add_argument('-i', '--disable_digest',
                        action='store_true',
                        help='生成目录描述文件时默认会生成文件的SHA512以供比较，有此选'
                             '项则不生成')
    export.add_argument('-d', '--differences',
                        action='store_true',
                        help='默认是生成目录描述文件，有此选项则是根据差异描述文件复制出'
                             '目录的差异部分')
    export.add_argument('-f', '--difference',
                        type=util.arg_type_file,
                        help='差异描述文件的完整路径，differences选项需要此选项')

    # import子命令，根据差异描述文件与远程目录的差异部分对本地目录进行差异管理
    import_ = subparsers.add_parser(
        'import',
        help='根据差异描述文件与远程目录的差异部分对本地目录进行差异管理',
        parents=[
            util.arg_log(),
            util.arg_log_identifier(),
            util.arg_shutdown(),
            common,
            common2,
            common5
        ]
    )
    import_.add_argument('dir',
                         type=util.arg_type_dir,
                         help='比较的基准目录的完整路径')
    import_.add_argument('dir2',
                         type=util.arg_type_dir,
                         help='远程目录的差异部分的目录的完整路径')
    import_.add_argument('difference',
                         type=util.arg_type_file,
                         help='差异描述文件的完整路径')

    # history子命令，比较目录描述文件看历史情况
    history = subparsers.add_parser(
        'history',
        help='比较目录描述文件看历史情况',
        parents=[
            util.arg_log(),
            util.arg_log_identifier(),
            common,
            common2
        ]
    )
    history.add_argument('description',
                         type=util.arg_type_file2,
                         help='基准目录描述文件，可提供本地文件的完整路径，也可提供一个'
                              'HTTP/HTTPS地址')
    history.add_argument('description2',
                        type=util.arg_type_file2,
                        help='与基准目录描述文件进行比较的目录描述文件，可提供本地文件的'
                             '完整路径，也可提供一个HTTP/HTTPS地址')

    global args
    args = parser.parse_args()
    args = check_args(
        args,
        eval(args.subcommand if args.subcommand != 'import' else 'import_'))

def log_config():
    if hasattr(args, 'log'):
        if hasattr(args, 'log_identifier') and args.log_identifier:
            return (
                '%s_v%s' % (__product__, __version__),
                args.log,
                args.log_identifier
            )
        else:
            return ('%s_v%s' % (__product__, __version__), args.log)

    return None

def init():
    log.info('program version: %s %s', __product__, __version__)
    log.debug('command line: %s', sys.argv)
    log.debug('parse args: %s', args)

    # 获取操作系统密码以便任务完成后自动关机
    if hasattr(args, 'shutdown'):
        global password
        password = util.get_os_password(args.shutdown)

    # 避免目标网站因HTTPS证书问题而导致请求失败
    ssl._create_default_https_context = ssl._create_unverified_context

    # 设置下载超时，这个是全局设置
    socket.setdefaulttimeout(60)

def schedule_tasks():
    if args.subcommand == 'local':
        local()
    elif args.subcommand == 'remote':
        remote()
    elif args.subcommand == 'export':
        export()
    elif args.subcommand == 'import':
        import_()
    elif args.subcommand == 'history':
        history()
    else:
        log.critical('schedule tasks ... failed ... exit')
        sys.exit(1)

def clean():
    # 自动关机
    if hasattr(args, 'shutdown'):
        util.shutdown(password)
